id, $pages, true) && strpos($screen->id, 'im-candidates') === false && strpos($screen->id, 'im-candidate-detail') === false) return; ?> '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); $c = IM_Candidate::get($id); if ($c && $c->status === 'screening') { IM_Candidate::update_status($id, 'invited'); } IM_Mailer::send_interview_invite($id) ? wp_send_json_success(['message' => '面试邀请邮件已成功发送!']) : wp_send_json_error(['message' => '邮件发送失败,请检查邮件配置。']); }); add_action('wp_ajax_im_action_reject', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Candidate::update_status($id, 'rejected'); IM_Mailer::send_reject($id) ? wp_send_json_success(['message' => '已拒绝该候选人并发送邮件!']) : wp_send_json_error(['message' => '已更新状态,但邮件发送失败。']); }); add_action('wp_ajax_im_action_hire', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Mailer::send_hire($id) ? wp_send_json_success(['message' => '已录取该候选人并发送培训链接邮件!']) : wp_send_json_error(['message' => '录取操作失败,请检查邮件配置。']); }); add_action('wp_ajax_im_action_resend_joinus', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Mailer::send_joinus_confirmation($id) ? wp_send_json_success(['message' => '表单链接邮件已重新发送!']) : wp_send_json_error(['message' => '邮件发送失败。']); }); add_action('wp_ajax_im_action_resend_training', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); $c = IM_Candidate::get($id); if (!$c || $c->status !== 'training') wp_send_json_error(['message' => '该候选人不在培训状态']); IM_Mailer::send_hire($id) ? wp_send_json_success(['message' => '培训链接邮件已重新发送!']) : wp_send_json_error(['message' => '邮件发送失败。']); }); add_action('wp_ajax_im_action_resend_trained_email', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); $c = IM_Candidate::get($id); if (!$c || $c->status !== 'trained') wp_send_json_error(['message' => '该候选人不在已完成培训状态']); IM_Mailer::send_training_complete($id) ? wp_send_json_success(['message' => '账号邮件已重新发送!']) : wp_send_json_error(['message' => '邮件发送失败。']); }); add_action('wp_ajax_im_action_delete', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Candidate::delete_with_files($id) ? wp_send_json_success(['message' => '候选人记录及关联文件已删除!']) : wp_send_json_error(['message' => '删除失败,请重试。']); }); /* ============================================================ 候选人列表页 ============================================================ */ function im_admin_list() { $page = max(1, (int) ($_GET['paged'] ?? 1)); $status = sanitize_text_field($_GET['status'] ?? ''); $search = sanitize_text_field($_GET['s'] ?? ''); $per_page = 20; $args = compact('page', 'status', 'search') + ['per_page' => $per_page]; $items = IM_Candidate::get_list($args); $total = IM_Candidate::count($args); $pages = (int) ceil($total / $per_page); $cnt = [ '' => IM_Candidate::count(), 'applied' => IM_Candidate::count(['status' => 'applied']), 'screening' => IM_Candidate::count(['status' => 'screening']), 'invited' => IM_Candidate::count(['status' => 'invited']), 'rejected' => IM_Candidate::count(['status' => 'rejected']), 'completed' => IM_Candidate::count(['status' => 'completed']), 'hired' => IM_Candidate::count(['status' => 'hired']), 'training' => IM_Candidate::count(['status' => 'training']), 'trained' => IM_Candidate::count(['status' => 'trained']), ]; $tabs = [ '' => '全部', 'applied' => '已申请', 'screening' => '待筛选', 'invited' => '已邀请', 'rejected' => '已拒绝', 'completed' => '已完成', 'hired' => '已录取', 'training' => '未完成培训', 'trained' => '已完成培训' ]; $status_lbl = [ 'applied' => '待完善信息', 'screening' => '已提交详细信息', 'invited' => '已邀请', 'rejected' => '已拒绝', 'completed' => '已完成', 'hired' => '已录取', 'training' => '未完成培训', 'trained' => '已完成培训' ]; $colors = [ 'applied' => '#f59e0b', 'screening' => '#8b5cf6', 'invited' => '#3b82f6', 'rejected' => '#ef4444', 'completed' => '#10b981', 'hired' => '#059669', 'training' => '#f97316', 'trained' => '#06b6d4' ]; ?>
| 姓名 | 邮箱 | 更新时间 | 状态 | 授课科目 / 详情 | 操作 |
|---|---|---|---|---|---|
| = $name_disp ?> preferred_name): ?> (= esc_html($c->preferred_name) ?>) | = esc_html($c->email) ?> | = esc_html(date('Y-m-d H:i', strtotime($c->updated_at))) ?> |
= $clabel ?>
status === 'applied' && !empty($c->apply_opened_at)): ?>
✓ 已打开表单
status === 'applied' && empty($c->apply_opened_at)): ?>
○ 未打开表单
status === 'invited'): ?>
id);
if (!empty($tk_list)):
$tk = $tk_list[0]; ?>
opened_at)): ?>
✓ 已查看面试题
○ 未点开面试链接
status === 'training'): ?>
training_opened_at)): ?>
✓ 已打开培训页面
○ 未打开培训页面
status === 'trained'): ?>
training_completed_at)): ?>
✓ = date('m/d H:i', strtotime($c->training_completed_at)) ?> 完成
|
status === 'applied'): ?> 等待完善信息 = esc_html($s) ?> 3): ?>+= count($subs) - 3 ?> — |
status !== 'applied'): ?>
查看详情
status === 'applied'): ?>
status === 'screening'): ?>
status === 'invited'): ?>
status === 'completed'): ?>
status === 'training'): ?>
status === 'trained'): ?>
apply_token_used) && $c->apply_token): ?>
status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])):
$latest_token = IM_Token::get_by_candidate($c->id);
if (!empty($latest_token)):
$tk2 = $latest_token[0]; ?>
status, ['training', 'trained']) && !empty($c->training_token)): ?>
|
| 暂无候选人数据 | |||||
候选人不存在。
该候选人尚未提交详细申请表,无法查看详情。
请决定是否邀请该候选人进行面试
如果候选人未收到邮件可重新发送
视频已提交,请查看并在通过后录用
候选人正在完成培训课程
training_opened_at)): ?>候选人已通过所有培训模块
training_completed_at)): ?>视频已脱机或不存在
在 server { } 块内添加以下配置,然后执行 sudo nginx -t && sudo nginx -s reload:
# 视频上传限制
client_max_body_size 512M;
client_body_timeout 300s;
send_timeout 300s;
# WordPress 固定链接(已有则跳过)
location / {
try_files $uri $uri/ /index.php?$args;
}
# 上传目录安全
location ~* ^/wp-content/uploads/(interviews|im-applications)/ {
add_header X-Content-Type-Options nosniff;
}
upload_max_filesize = 500M
post_max_size = 512M
max_execution_time = 300
max_input_time = 300
memory_limit = 256M
修改后重启 PHP-FPM:sudo systemctl restart php8.x-fpm
需在 WordPress 后台创建以下两个页面:
| 页面 | Shortcode | 建议 URL slug |
|---|---|---|
| Join Us 报名页 | [im_joinus_form] |
/join-us/ |
| 详细申请表单页 | [im_apply_form] |
/apply/ |
| 面试页 | [im_interview] |
/interview/ |
| 培训页 | [im_training] |
/training/ |
创建好页面后,在片段 1 顶部将对应常量改为实际 URL:
define('IM_APPLY_PAGE_URL', home_url('/apply/'));define('IM_INTERVIEW_PAGE_URL', home_url('/interview/'));define('IM_TRAINING_PAGE_URL', home_url('/training/'));define('IM_TRAINING_ACCOUNT', 'your_account@example.com');define('IM_TRAINING_PASSWORD', 'your_password');
| 配置项 | 当前值 | 状态 |
|---|---|---|
= $name ?> |
= esc_html($val) ?> | = $ok ? '✅' : '⚠️ ' . $tip ?> |
| 视频存储目录 | = esc_html($upload_dir['basedir'] . '/interviews/') ?> | = is_dir($upload_dir['basedir'] . '/interviews') ? (is_writable($upload_dir['basedir'] . '/interviews') ? '✅ 可写' : '❌ 不可写') : '(首次上传时自动创建)' ?> |
| 申请文件目录 | = esc_html($upload_dir['basedir'] . '/im-applications/') ?> | = is_dir($upload_dir['basedir'] . '/im-applications') ? (is_writable($upload_dir['basedir'] . '/im-applications') ? '✅ 可写' : '❌ 不可写') : '(首次上传时自动创建)' ?> |
| 面试页面 URL | = esc_html(IM_INTERVIEW_PAGE_URL) ?> | — |
| 培训页面 URL | = esc_html(IM_TRAINING_PAGE_URL) ?> | — |
| 培训临时账号 | = esc_html(IM_TRAINING_ACCOUNT) ?> | = IM_TRAINING_ACCOUNT !== 'your_account@example.com' ? '✅' : '⚠️ 请配置实际账号' ?> |